<?php
/**
 * Dependency cli service
 *
 * PHP version 5
 *
 * @category  W
 * @package   Services
 * @author    Olivier Hoareau <olivier@phppro.fr>
 * @copyright 2009 PHPPRO http://www.phppro.fr
 * @license   http://dev.phppro.fr/license PHPPRO
 * @version   SVN: $Id:$
 * @link      http://dev.phppro.fr/projects/wdependency
 */
require_once 'W/W.php';
require_once 'W/Dependency/Cli/Abstract.php';
/**
 * Dependency cli service
 *
 * @category  W
 * @package   Services
 * @author    Olivier Hoareau <olivier@phppro.fr>
 * @copyright 2009 PHPPRO http://www.phppro.fr
 * @license   http://dev.phppro.fr/license PHPPRO
 * @version   SVN: $Id$
 * @link      http://dev.phppro.fr/projects/wdependency
 */
class W_Dependency_Cli_Service extends W_Dependency_Cli_Abstract
{
    /**
     * Lists all plugin for specified options on command line.
     * Will return an array, for example:
     *
     * array(
     *     // list of plugins by type
     *     0 => array(
     *         array(
     *             'filter' => array(
     *                 0 => array(
     *                     array('exactLinks',array('value'=>12)),
     *                 ),
     *             'aggregator' => array(
     *             ),
     *         ),
     *     // cleaned options array
     *     1 => array(
     *         'unknownOption' => 'value',
     *     ),
     * )
     *
     * @param array $options the options
     *
     * @return array containing plugins list and cleaned options
     *
     * @throws Exception if an error occured
     */
    public function listPluginsForOptions($options)
    {
        $plugins = array();

        list($cmds,$options) = $this->formatCommands($options);
        $options             = $this->formatOptions($options);

        foreach($cmds as $cmd) {
            $options = $this->updateOptionsForCommand($cmd,$options);
        }

        foreach(array_keys($options) as $name) {
            if (false === isset($options[$name])) {
                continue;
            }
            list($items,$options) = $this->listPluginsForOption($name,$options[$name],$options);
            if (null !== $items && true === is_array($items) &&
                0 < count($items)) {
                $plugins = array_merge($plugins, $items);
            }
        }

        $items = array();

        foreach($plugins as $plugin) {
            $type = array_shift($plugin);
            if (false === isset($items[$type])) {
                $items[$type] = array();
            }
            $items[$type][] = $plugin;
        }

        return array($items,$options);
    }
    public function formatCommands($options)
    {
        $cmds = array();
        if (true === isset($options['=commands='])) {
            $cmds = $options['=commands='];
            unset($options['=commands=']);
        }
        return array($cmds,$options);
    }
    /**
     * Formats the options
     *
     * @return array
     */
    public function formatOptions($options)
    {
        // checks
        if (false === isset($options['check'])) {
            $options['check'] = array();
        }
        if (false === is_array($options['check'])) {
            $options['check'] = explode(',',$options['check']);
        }

        // formats
        if (false === isset($options['format'])) {
            $options['format'] = array();
        }
        if (false === is_array($options['format'])) {
            $options['format'] = explode(',',$options['format']);
        }

        // directories
        if (false === isset($options['directory'])) {
            $options['directory'] = array();
        }
        if (false === is_array($options['directory'])) {
            $options['directory'] = explode(',',$options['directory']);
        }

        // include paths
        if (false === isset($options['path'])) {
            $options['path'] = array();
        }
        if (false === is_array($options['path'])) {
            $options['path'] = explode(PATH_SEPARATOR,$options['path']);
        }

        return $options;
    }
    public function updateOptionsForCommand($cmd,$options)
    {
        if (0 === count($options['format'])) {
            $options['format'] = array('png');
        }
        if (0 === count($options['directory'])) {
            $options['directory'] = array('.');
        }
        switch($cmd) {
            case 'help': $options = array('h'=>true); break;
            case 'instances':
            case 'packages':
            case 'includes':
            case 'classes':
            case 'inheritances':
                $options['check'] = array($cmd);
                break;
            case 'package0-inheritances':
                $options['check'] = array('inheritances');
                $options['by']    = 'package#0';
                break;
            case 'used':
                $options['check']   = array('dependencies');
                $options['reverse'] = true;
                break;
            case 'hubs':
                $options['check']   = array('classes');
                $options['reverse'] = true;
                $options['top']     = 5;
                break;
            case 'dependent-nodes':
                $options['check']   = array('dependencies');
                $options['top']     = 5;
                break;
            case 'supers':
                $options['check']   = array('inheritances');
                $options['reverse'] = true;
                break;
            case 'often-used':
                $options['check']   = array('dependencies');
                $options['reverse'] = true;
                $options['top']     = 5;
                break;
            case 'package1':
                $options['check'] = array('classes');
                $options['by']    = 'package#1';
                break;
            case 'dependent':
                $options['check']           = array('classes');
                $options['min-link-weight'] = 4;
                break;
            case 'very-dependent':
                $options['check']           = array('classes');
                $options['min-link-weight'] = 10;
                break;
            case 'extra-dependent':
                $options['check']           = array('classes');
                $options['min-link-weight'] = 20;
                break;
            case 'testcases':
                $options['check']     = array('inheritances');
                $options['linked-to'] = 'PHPUnit_Framework_TestCase';
                break;
            case 'testedpackages':
                $options['check'] = array('inheritances');
                $options['by']        = 'package#1';
                $options['linked-to'] = 'PHPUnit_Framework';
                break;
            case 'untestedpackages':
                $options['check']         = array('inheritances');
                $options['by']            = 'package#1';
                $options['not-linked-to'] = 'PHPUnit_Framework';
                break;
            case 'self-dependent':
                $options['check']     = array('classes');
                $options['linked-to'] = '@self';
                break;
            case 'describe-listener':   $options['show-interface'] = 'listener'; break;
            case 'describe-analyzer':   $options['show-interface'] = 'analyzer'; break;
            case 'describe-exporter':   $options['show-interface'] = 'exporter'; break;
            case 'describe-aggregator': $options['show-interface'] = 'aggregator'; break;
            case 'describe-filter':     $options['show-interface'] = 'filter'; break;
            case 'describe-action':     $options['show-interface'] = 'action'; break;
            case 'describe-source':     $options['show-interface'] = 'source'; break;
            case 'check-dot-support':   $options['check-dot-support'] = true; break;
            case 'popo':
                $options['check']  = array('inheritances');
                $options['none']   = true;
                $options['layout'] = 'neato';
                break;
            case 'cache':
                $options['c'] = true;
                break;
            case 'clean-cache': $options['cc'] = true; break;
        }

        return $options;
    }
    /**
     * Lists all plugins for specified option/value.
     * Will return an array, for example:
     *
     * array(
     *     0 => array('filter'    , 'moreThanLinks', array('value'=>12)),
     *     1 => array('aggregator', 'directory     , array()),
     *     2 => array('filter'    , 'exactLinks'   , array('value'=>12)),
     * )
     *
     * @param string $name    option name
     * @param mixed  $value   option value
     * @param array  $options the context
     *
     * @return array of plugins
     *
     * @throws Exception if an error occured
     */
    public function listPluginsForOption($name,$value,$options)
    {
        $items = array();
        switch($name) {
            // flags
            case 'v':
                W_W::ctx()->set('logger.file','./wdependency.log')->set('verbose',true);
                W_W::log()->log("WDependency started.",'debug',__CLASS__);
                $items[] = array('listener','events',array());
                break;
            case 'h':
                $options = array();
                break;
            case 'cc':
                $items[] = array('action','cleanCache',array()); break;
            case 'c':
                if (true === W_W::fs()->isExistingFile('wdependency.cache')) {
                    $items[] = array('source','file_Php', array('file'=>'wdependency.cache'));
                }else{
                    $check = 'all';
                    $options['check'] = array($check);
                        foreach(array('includes' , 'inheritances' , 'dependencies' , 'instances' , 'types' , 'packages','projects','classes') as $action) {
                            $items[] = array('analyzer',$action,array('key'=>$action));
                        }
                    $items[] = array('analyzer',$check,array('key'=>$check));
                    foreach ($options['check'] as $check) {
                        $items[] = array('exporter','php',array_merge($options,array('key' => $check, 'output' => 'wdependency.cache' )));
                    }
                }
                break;
            // valued
            case 'show-interface':
                $items[] = array('action','showInterface',array('class'=>'W_Dependency_'.ucfirst($value)));
                break;
            case 'check':
                foreach($value as $check) {
                    if ('all'===$check) {
                        foreach(array('includes' , 'inheritances' , 'dependencies' , 'instances' , 'types' , 'packages','projects','classes') as $action) {
                            if (false === in_array($action,$value)) {
                                $items[] = array('analyzer',$action,array('key'=>$action));
                            }
                        }
                    }
                    $items[] = array('analyzer',$check,array('key'=>$check));
                }
                break;
            case 'load':
                if (false === W_W::fs()->isExistingFile($value)) {
                    throw new RuntimeException("File '$value' does not exist");
                }
                $items[] = array('source','file_'.ucfirst(substr($value,strrpos($value,'.')+1)), array('file'=>$value));
                break;
            case 'min':    $items[] = array('filter','moreThanLinks',      array('value'=>$value));      break;
            case 'max':    $items[] = array('filter','lessThanLinks',      array('value'=>$value));      break;
            case 'n':      $items[] = array('filter','exactLinks',         array('value'=>$value));      break;
            case 'none':   $items[] = array('filter','noLinks',            array());                     break;
            case 'has':    $items[] = array('filter','hasLinks',           array());                     break;
            case 'not':    $items[] = array('filter','differentLinks',     array('value'=>$value));      break;
            case 'file':
            case 'class':
            case 'node':   $items[] = array('filter','nodeNameEquals',     array('name'=>$value));       break;
            case 'package':$items[] = array('filter','nodeNamePrefixed',   array('prefix'=>$value.'_')); break;
            case 'dir':    $items[] = array('filter','nodeNamePrefixed',   array('prefix'=>$value.'/')); break;
            case 'prefix': $items[] = array('filter','nodeNamePrefixed',   array('prefix'=>$value));     break;
            case 'pattern':$items[] = array('filter','nodeNameMatches',    array('pattern'=>$value));    break;
            case 'link':   $items[] = array('filter','hasSpecificLink',    array('link'=>$value));       break;
            case 'nolink': $items[] = array('filter','hasNotSpecificLink', array('link'=>$value));       break;
            case 'top':    $items[] = array('filter','TopRank',            array('size'=>$value));       break;
            case 'bottom': $items[] = array('filter','BottomRank',         array('size'=>$value));       break;
            case 'filter': $items[] = array('filter',$value,               $options);                    break;
            case 'linked-to': $items[] = array('filter','linkedTo',        array('link'=>$value));       break;
            case 'listener':$items[] = array('listener',$value,            $options);                    break;
            case 'analyzer':$items[] = array('analyzer',$value,            $options);                    break;
            case 'exporter':$items[] = array('exporter',$value,            $options);                    break;
            case 'aggregator':$items[] = array('aggregator',$value,        $options);                    break;
            case 'source':$items[] = array('source',$value,                $options);                    break;
            case 'action':$items[] = array('action',$value,                $options);                    break;
            case 'min-link-weight': $items[] = array('filter','linksWithMinWeight',array('weight'=>$value)); break;
            case 'max-link-weight': $items[] = array('filter','linksWithMaxWeight',array('weight'=>$value)); break;
            case 'by':
                switch($value) {
                    case 'dir':          $items[] = array('aggregator','directory',array()); break;
                    case 'package#' === substr($value,0,strlen('package#')):
                        $items[] = array('aggregator','directory',array('sep'=>'_','depth'=>substr($value,strlen('package#')))); break;
                    case 'package':      $items[] = array('aggregator','directory',array('sep'=>'_')); break;
                    case 'dir#' === substr($value,0,strlen('dir#')):
                        $items[] = array('aggregator','directory',array('depth'=>substr($value,strlen('dir#')))); break;
                    default:
                        throw new RuntimeException("Unsupported aggregator 'by $value'");
                }
                break;
            case 'format':
                foreach ($options['check'] as $check) {
                    foreach ($value as $format) {
                        $items[] = array('exporter',$format,array_merge($options,array('key' => $check )));
                    }
                }
                break;
            case 'subdirs-as-projects':
                $new = array();
                foreach($options['directory'] as $dir) {
                    $dirs = scandir($dir);
                    foreach($dirs as $d) {
                        if ('.' !== $d && '..' !== $d && '.svn' !== $d && 'CVS' !== $d) {
                            if (true === is_dir($dir.'/'.$d)) {
                                $new[] = $dir.'/'.$d;
                            }
                        }
                    }
                }
                $options['directory'] = $new;
                break;
            case 'reverse': $items[] = array('filter','reverse',array()); break;

            // hidden
            case 'check-dot-support': $items[] = array('action','displayDotVersion',array()); break;
        }
        return array($items,$options);
    }
    public function getUsage()
    {
        return 'wdependency [commands] [options] [<directory>[,<directory>,...]]';
    }
    public function listAvailableOptions()
    {
        return array(
            'flags' => array(
                'v' => 'verbose mode, debug information logged in ./wdependency.log',
                'h' => 'display this help',
                'c' => 'activate/use cache file for parsing optimization (generate/use ./wdependency.cache file)',
                'cc'=> 'clean existing cache file',
            ),
            'commands'=>array(
                'help' => 'display this help',
                'instances'=>'computes the instances (use of new operator) dependencies',
                'cache' => 'parses directory content and caches result in all.php for further use',
                'clean-cache' => 'cleans existing cache file',
                'often-used' => 'top rank node that heavily depend on others',
                'classes' => 'computes the classes dependencies (children => mother)',
                'package0-inheritances'=> 'computes the classes inheritances reduced to first package level',
            	'package1'=> 'computes the classes dependencies reduced to second package level',
                'inheritances' => 'computes the inheritances dependencies',
                'includes' => 'computes the includes dependencies',
                'packages' => 'computes the packages dependencies',
                'dependent-nodes' => 'list all mostly dependent nodes (top ranked)',
            	'check-dot-support'=>'display the version of dot binary if available (required for dot/picture export)',
                'popo'=>'list Plain Old PHP Objects (no extends / no implements)',
                'supers'=>'computes the hierarchy dependency (mother => children)',
                'testcases'=>'list all classes that extends PHPUnit_Framework_TestCase',
                'untestedpackages'=>'list all packages that does not contain phpunit test cases',
            	'testedpackages'=>'list all packages that contains phpunit test cases',
                'hubs'=>'list all classes that are very used by others',
            	'self-dependent'=>'list all classes that are self-dependent (depend on themself)',
            	'dependent'=>'list all classes that are dependent to others (more than 4 links to the same)',
            	'very-dependent'=>'list all classes that are very dependent to others (more than 10 links to the same)',
                'extra-dependent'=>'list all classes that are extra dependent to others (more than 20 links to the same)',
            	'describe-X'=>array('display content of specific interface, where X is:',array(
                	'listener'=>'for displaying W_Dependency_Listener_Interface',
                	'exporter'=>'for displaying W_Dependency_Exporter_Interface',
                	'analyzer'=>'for displaying W_Dependency_Analyzer_Interface',
                	'filter'=>'for displaying W_Dependency_Filter_Interface',
                	'aggregator'=>'for displaying W_Dependency_Aggregator_Interface',
                	'source'=>'for displaying W_Dependency_Source_Interface',
                	'action'=>'for displaying W_Dependency_Action_Interface',
            )),
            ),
            'valued' => array(
                'load' => "loads the specified file as a data source (from previous analyzes, PHP format supported only)",
                'min'  => "keep only nodes that have at least the minimum links specified",
                'max'  => "keep only nodes that have a maximum of links specified",
                'n'    => "keep only nodes that have number of nodes specified",
                'none' => "keep only nodes that have no links",
                'has'  => "keep only nodes that have links",
                'file' => "keep only nodes that are used by specified file",
                'class'=> "keep only nodes that are used by specified class",
                'format'=>array("list of export format:",array(
                    'png'=>'PNG picture file',
                    'jpg'=>'JPG picture file',
                    'gif'=>'GIF picture file',
                    'dot'=>'GraphViz DOT text format',
                    'graphml'=>'GraphML xml format',
                    'json'=>'JavaScript Object Notation text format',
                    'php'=>'PHP file (return array(...);)',
                    'svg'=>'Scalable Vector Graphics picture format (xml)',
                )),
                'node' => "keep only nodes that are used by specified node",
                'package' => "keep only nodes that are used by class in specified package",
                'dir' => "keep only nodes that are used by file in specified directory",
                'prefix' => "keep only nodes that have name beginning with specified prefix",
                'pattern' => "keep only nodes that have name matching specified pattern",
                'link' => "keep only nodes that have link to specified node",
                'nolink' => "keep only nodes that have not link to specified node",
                'linked-to' => "keep only links to specified dependency",
                'min-link-weight'=>"keep only links that have more than specified weight",
                'max-link-weight'=>"keep only links that have less than specified weight",
                'top' => "keep only nodes that are in the specified top rank (top X) when computing weight from links",
                'bottom' => "keep only nodes that are in the specified bottom rank (top X) when computing weight from links",
                'by'=> array('aggregate nodes using the specified algorithm:',array(
                    'dir'=>'aggregate by directories (dirname on / of node)',
                    'package'=>'aggregate by package (dirname on _ of node)',
                    'package#X'=>'aggregate by package at position X',
                    'dir#X'=>'aggregate by directory at position X',
                )),
                'subdirs-as-projects'=>"use subdirectories as autonomous projects",
                'template'=>"php file template to render export (file exporter only), use \$options and \$data",
                'layout'=>"graphviz (dot) layout to use, by default 'dot', type dot -K? to see available list",
                'reverse'=>"reverse link order (user become used and used become user)",
                'filter'=>"load custom filter(s) (list separator: ,), specified by class name",
                'listener'=>"load custom listener(s) (list separator: ,), specified by class name",
                'analyzer'=>"load custom analyzer(s) (list separator: ,), specified by class name",
                'exporter'=>"load custom exporter(s) (list separator: ,), specified by class name",
                'aggregator'=>"load custom aggregator(s) (list separator: ,), specified by class name",
                'source'=>"load custom source(s) (list separator: ,), specified by class name",
                'action'=>"load custom action(s) (list separator: ,), specified by class name",
                ),
        );
    }
    public function displayUsage()
    {
        $usage = $this->getUsage();
        $options = $this->listAvailableOptions();
        ob_start();
        include dirname(__FILE__).'/resources/display-usage.phtml';
        $content = ob_get_contents();
        ob_end_clean();
        echo $content;
    }
    public function parseCommandLineArguments($argv)
    {
        array_shift($argv);
        $params = array();
        foreach(array_keys($argv) as $v ) {
                $matches = null;
                if (false === isset($argv[$v])) {
                    continue;
                }
                if (0 < preg_match("|^\\-\\-([^=]+)=(.*)$|",$argv[$v],$matches)) {
                    $a = $matches[2];
                    if ('true'===$a) {
                        $a = true;
                    }elseif('false' === $a) {
                        $a = false;
                    }
                    $params[$matches[1]] = $a;
                    unset($argv[$v]);
                    continue;
                }elseif (0 < preg_match("|^\\-([^=]+)$|",$argv[$v],$matches)) {
                    $params[$matches[1]] = true;
                    unset($argv[$v]);
                    continue;
                }elseif (0 < preg_match("|^\\:([^=]+)$|",$argv[$v],$matches)) {
                    if (false === isset($params['=commands='])) {
                        $params['=commands='] = array();
                    }
                    $params['=commands='][] = $matches[1];
                    unset($argv[$v]);
                    continue;
                }
        }
        $options = $params;
        $options = array_merge($options,array(
                    'directory' => 0 === count($argv) ? null : array_shift($argv),
                ));
        return $options;
    }
}
